package orbito.simulation;

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;

import orbito.GameController;
import orbito.graphics.Viewport;
import orbito.util.Stopwatch;

public class ThreadedSimulator implements Simulator, Runnable {
	
	// The game controller.
	GameController game;
	
	// If the simulator is running, thread has terminated. 
	boolean isRunning, hasTerminated;
	
	// If the simulator is resetting, pausing.
	boolean invokeReset, invokePause;
	
	// If the simulation in running in-real time, this is set to true, false otherwise.
	boolean isRunningRealTime;
	
	// The initially set dt, the current dt.
	final double initDt;
	double dt;
	
	// The average dt, for display.
	double displayDt;
	
	// The time the simulation has run, in seconds.
	double simTime;

	// The time accumulator for accumulating enough sim-time to perform a step.
	double simTimeAccum;
	
	// The time compression.
	double timeCompression;
	
	// The overall number of simulation steps performed.
	long stepCount;
	
	// Step size lock in down and up directions.
	boolean dtDownLock = false, dtUpLock = false;
	
	// Stopwatches for timing and benchmarking the simulation.
	Stopwatch sw, benchSw;
	
	public ThreadedSimulator(double initDt) {
		// Set initial dt.
		this.initDt = initDt;
		
		// Init simulator.
		init();
		
		// Start thread.
		Thread thread = new Thread(this);
		thread.setPriority(Thread.MIN_PRIORITY);
		thread.start();
	}
	
	public void init() {
		// Init time variables.
		dt = initDt;
		simTime = 0;
		simTimeAccum = 0;
		timeCompression = 1;
		stepCount = 0;
		
		// Init sim states.
		isRunning = true;
		hasTerminated = false;
		invokePause = true;
		invokeReset = false;
		isRunningRealTime = true;
		
		// Init stopwatches.
		sw = new Stopwatch();
		benchSw = new Stopwatch();
	}
	
	public void run() {
		// Run until the thread is stopping.
		while (isRunning) {
			
			// Init the simulation.
			init();
			
			// Run until the simulation resets.
			while (true) {
				
				// Wait for anyone that is changing the state of the simulator.
				synchronized (this) {
					
					// Wait until thread is unpaused.
					while (invokePause) {
						try { wait(); }
						catch (InterruptedException ex) { }
					}
					
					// Break the loop if the simulator is resetting.
					if (invokeReset)
						break;
					
					// Determine the time passed since the last frame.
					double realElapsedTime = sw.getElapsedTime();
					
					// Reset the benchmark stopwatch.
					benchSw.reset();
					
					// Determine the sim time passed and schedule it for simulation.
					double simElapsedTime = timeCompression * realElapsedTime;
					simTimeAccum += simElapsedTime;
					
					double simTimeToProcess = simTimeAccum;
					
					while (simTimeAccum > dt) {
						game.step(simTime, dt);

						simTimeAccum -= dt;
						simTime += dt;
						stepCount++;

						if (benchSw.peekElapsedTime() * timeCompression > simTimeToProcess * 0.75) {
							dt *= 1.1;
							if (dtUpLock) {
								simTimeAccum = 0;
							}
						}
						
						if (dtUpLock && dt > initDt) {
							dt = initDt;
						}
					}
					
					if (benchSw.peekElapsedTime() * timeCompression < simTimeToProcess * 0.25) {
						dt *= 0.9;
					}
					
					if (dtDownLock && dt < initDt) {
						dt = initDt;
					}
				}
				
				// Smooth the display dt.
				displayDt = displayDt * 0.95 + dt * 0.05;
				
				// Give the system some time to breathe.
				Thread.yield();
			}
		}
		
		hasTerminated = true;
	}
	
	public void step() {
		// Nothing to do here.
	}
	
	public void renderInfo(Graphics2D g, Viewport v, AffineTransform tx, double t, double dt) {
		AffineTransform tr = g.getTransform();
		int labelX = 0, valueX = 100, labelY = 0, h = g.getFontMetrics().getHeight();

		// Translate to upper right side of viewport.
		g.translate(v.getWidth() - 250, 30);
		
		String elapsedLabel = "Elapsed Time:";
		String elapsedValue = String.format("%.2f s", simTime);
		
		String tcmpLabel = "Time Compression:";
		String tcmpValue = timeCompression < 1 ? 
				String.format("1/%.0fx", 1 / timeCompression) :
				String.format("%.0fx", timeCompression);
		
		String stepLabel = "Time Step:";
		String stepValue = String.format("%.6f s", displayDt);
		
		String accumLabel = "Accumulator:";
		String accumValue = String.format("%.6f s", simTimeAccum);
		
		String stepCountLabel = "Step Count:";
		String stepCountValue = String.format("%d", stepCount);
		
		g.setColor(Color.ORANGE);
		g.drawString(elapsedLabel, labelX, labelY += h);
		g.drawString(tcmpLabel, labelX, labelY += h);
		g.drawString(stepLabel, labelX, labelY += h);
		g.drawString(accumLabel, labelX, labelY += h);
		g.drawString(stepCountLabel, labelX, labelY += h);
		
		labelY = 0;
		
		g.drawString(elapsedValue, valueX, labelY += h);
		
		if (!isRunningRealTime()) {
			Color c = g.getColor();
			g.setColor(Color.RED);
			g.drawString(tcmpValue + " (limited)", valueX, labelY += h);
			g.setColor(c);
		} else {
			g.drawString(tcmpValue, valueX, labelY += h);
		}
		
		g.drawString(stepValue, valueX, labelY += h);
		g.drawString(accumValue, valueX, labelY += h);
		g.drawString(stepCountValue, valueX, labelY += h);
		
		g.setTransform(tr);
	}
	
	public synchronized void increaseTimeCompression() {
		if (isRunningRealTime) {
			timeCompression *= 2;
		}
	}
	
	public synchronized void decreaseTimeCompression() {
		timeCompression /= 2;
	}
	
	public synchronized void resetTimeCompression() {
		timeCompression = 1;
	}
	
	public double getTimeCompression() {
		return timeCompression;
	}
	
	public double getTime() {
		return simTime;
	}
	
	public double getTimeStep() {
		return dt;
	}
	
	public long getStepCount() {
		return stepCount;
	}
	
	/**
	 * Gets the ratio between accumulated time and the time step size. Essentially it returns how 
	 * close the simulator is to perform a time step, typically in [0, 1].
	 * 
	 * If the time between simulation steps is fairly big, this can be used to interpolate between two states 
	 * when rendering, so as to make the animation smooth, even when the simulation is running slowly.
	 * 
	 * @return The ratio of accumulated time to time step size.
	 */
	public double getAccumulatorRatio() {
		return simTimeAccum / dt;
	}
	
	public synchronized void setDtDownLock(boolean b) {
		dtDownLock = b;
	}
	
	public boolean isDtDownLock() {
		return dtDownLock;
	}
	
	public synchronized void setDtUpLock(boolean b) {
		dtUpLock = b;
	}
	
	public boolean isDtUpLock() {
		return dtUpLock;
	}
	
	public boolean isRunningRealTime() {
		return isRunningRealTime;
	}
	
	public boolean isRunning() {
		return isRunning;
	}
	
	public boolean isPaused() {
		return invokePause;
	}
	
	public boolean isResetting() {
		return invokeReset;
	}
	
	public boolean hasTerminated() {
		return hasTerminated;
	}
	
	public synchronized void start() {
		sw.reset();
		invokePause = false;
		
		// Wake up paused threads.
		notifyAll();
	}
	
	public synchronized void pause() {
		invokePause = true;
	}
	
	public synchronized void reset() {
		invokePause = false;
		invokeReset = true;
		
		// Wake up any paused thread.
		notifyAll();
	}
	
	public synchronized void terminate() {
		invokePause = false;
		invokeReset = true;
		isRunning = false;
		
		// Wake up paused threads.
		notifyAll();
	}
	
	public void setGameController(GameController game) {
		this.game = game;
	}
}
